//
// Unihan Input Method
//
// Copyright © 2002-2007 Sun Wah Linux Ltd.
//
//
// This library is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation; either version 2.1 of the
// License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
// USA
//
// $Id: unicode.c 800 2007-07-25 02:52:29Z roger $
//

#ifdef HAVE_CONFIG_H
#  include <config.h>
#endif

#include "unicode.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

/**
 * @defgroup Unicode Unicode handling
 * @{
 */
/**
 * @brief Converts a sequence of bytes encoded as UTF-8 to a Unicode character.
 *
 * If \a in does not point to a valid UTF-8 character, results are undefined.
 *
 * @param in    a pointer to Unicode character encoded as UTF-8
 *
 * @returns the resulting Unicode character.
 */
static UTF32CHAR
utf8_get_char(const UTF8CHAR *in)
{
	UTF32CHAR u32 = 0;
	int i, u8_len;

	if (*in < 128) {
		u8_len = 1;
		u32 = *in & 0x7F;
	} else if ((*in & 0xE0) == 0xC0) {
		u8_len = 2;
		u32 = *in & 0x1F;
	} else if ((*in & 0xF0) == 0xE0) {
		u8_len = 3;
		u32 = *in & 0x0F;
	} else if ((*in & 0xF8) == 0xF0) {
		u8_len = 4;
		u32 = *in & 0x07;
	} else if ((*in & 0xFC) == 0xF8) {
		u8_len = 5;
		u32 = *in & 0x03;
	} else if ((*in & 0xFE) == 0xFC) {
		u8_len = 6;
		u32 = *in & 0x01;
	} else {	// invalid UTF-8!
		return 0;
	}

	for (i = 1; i < u8_len; ++i) {
		if ((in[i] & 0xC0) != 0x80)	// not 10xxxxxx, something wrong
			return 0;
		u32 <<= 6;
		u32 |= (in[i] & 0x3F);
	}

	return u32;
}


/**
 * @brief Finds the next UTF-8 character in the string after \a in.
 * 
 * No check is made to see if the character found is actually valid.
 *
 * @param in    a pointer to a position within a UTF-8 string
 *
 * @returns a pointer to the found character, or %NULL if \a in points to an
 * invalid UTF-8 string.
 */
const UTF8CHAR *
utf8_next_char(const UTF8CHAR *in)
{
	if (*in < 128)
		return (in + 1);
	else if ((*in & 0xE0) == 0xC0)
		return (in + 2);
	else if ((*in & 0xF0) == 0xE0)
		return (in + 3);
	else if ((*in & 0xF8) == 0xF0)
		return (in + 4);
	else if ((*in & 0xFC) == 0xF8)
		return (in + 5);
	else if ((*in & 0xFE) == 0xFC)
		return (in + 6);

	// invalid!
	return NULL;
}


/**
 * @brief Converts a sequence of bytes encoded as UTF-16 in the host byte order
 * to a Unicode character.
 *
 * If \a in does not point to a valid UTF-16 character, results are undefined.
 *
 * @param in    a pointer to Unicode character encoded as UTF-16
 *
 * @returns the resulting Unicode character.
 */
static UTF32CHAR
utf16_get_char(const UTFCHAR *in)
{
        if (*in < 0xD800 || *in > 0xDFFF)
                return *in;

        // surrogate
        return (*in - 0xD800) * 0x400 + (in[1] - 0xDC00) + 0x10000;
}


/**
 * @brief Finds the next UTF-16 character in the string after \a in.
 * 
 * No check is made to see if the character found is actually valid.
 *
 * @param in    a pointer to a position within a UTF-16 string
 *
 * @returns a pointer to the found character, or %NULL if \a in points to an
 * invalid UTF-16 string.
 */
static const UTFCHAR *
utf16_next_char(const UTFCHAR *in)
{
        if (*in < 0xD800 || *in > 0xDFFF)
                return (in + 1);
        return (in + 2);
}


/**
 * @brief Converts a string from UTF-8 to UTF-16BE.
 *
 * @param[in]  s            current IMLSession for allocating memory for the
 *                          new string. If %NULL, the new string will be
 *                          allocated using malloc().
 * @param[in]  utf8         a pointer to a UTF-8 string.
 * @param[out] num_chars    location to store the number of UTF-16 characters
 *                          written, or %NULL
 *
 * @returns a pointer to the newly allocated UTF-16 string. If an error occurs,
 * %NULL will be returned.
 */
UTFCHAR *
utf8_to_utf16be(iml_session_t *s, const UTF8CHAR *utf8, unsigned int *num_chars)
{
	const UTF8CHAR *in;
	int utf16_len = 0;
	UTFCHAR *out, *utf16 = NULL;
	int c = 0;
	
	// Calculate how much space we need to allocate for the new string
	// To do that we need to see how many surrogates are needed
	in = utf8;
	utf16_len = 0;
	while (in && *in) {
		UTF32CHAR utf32 = utf8_get_char(in);

		if (utf32 < 0xD800) {
			utf16_len += 1;
		} else if (utf32 < 0xE000) {
			// surrogate char? invalid
			return NULL;
		} else if (utf32 < 0x10000) {
			utf16_len += 1;
		} else if (utf32 < 0x110000) {
			utf16_len += 2;
		} else {
			// outside range, invalid
			return NULL;
		}

		in = utf8_next_char(in);
	}
	
	if (s)
		utf16 = (UTFCHAR *)s->If->m->iml_new(s, sizeof(UTFCHAR) * (utf16_len + 1));
	else
		utf16 = (UTFCHAR *)malloc(sizeof(UTFCHAR) * (utf16_len + 1));
	memset(utf16, 0, sizeof(UTFCHAR) * (utf16_len + 1));

	// Now do the actual conversion
	in = utf8;
	out = utf16;
	while (in && *in) {
		UTF32CHAR utf32 = utf8_get_char(in);

		if (utf32 < 0x10000) {
			*out = utf32; ++out;
		} else {
			*out = (utf32 >> 10) + 0xD800 - 0x40; 
            ++out;
			*out = (utf32 & 0x3FF) + 0xDC00; 
            ++out;
		}
		++c;
		in = utf8_next_char(in);
	}
	*out = 0;

	if (num_chars)
		*num_chars = utf16_len;
	return utf16;
}


/**
 * @brief Converts a string from UTF-8 to UTF-32.
 *
 * @param s         current IMLSession for allocating memory for the new
 *                  string. If %NULL, the new string will be allocated using
 *                  malloc().
 * @param utf8      a pointer to a UTF-8 string.
 * @param num_chars location to store the number of UTF-32 characters written,
 *                  or %NULL
 *
 * @returns a pointer to the newly allocated UTF-32 string. If an error occurs,
 * %NULL will be returned.
 */
UTF32CHAR *
utf8_to_utf32(iml_session_t *s, const UTF8CHAR *utf8, unsigned int *num_chars)
{
	const UTF8CHAR *in;
	int utf32_len = 0;
	UTF32CHAR *out, *utf32 = NULL;
	int c = 0;
	
	// Calculate how much bytes we need to allocate for the new string
	// To do that we only need to see how many Unicode characters there
        // are, then times by 4
	in = utf8;
	utf32_len = 0;
	while (*in) {
                // FIXME: security check?
                utf32_len += 4;
		in = utf8_next_char(in);
	}
	
	if (s)
		utf32 = (UTF32CHAR *)s->If->m->iml_new(s, sizeof(UTF32CHAR) * (utf32_len + 1));
	else
		utf32 = (UTF32CHAR *)malloc(sizeof(UTF32CHAR) * (utf32_len + 1));
	memset(utf32, 0, sizeof(UTF32CHAR) * (utf32_len + 1));

	// Now do the actual conversion
	in = utf8;
	out = utf32;
	while (*in) {
		UTF32CHAR utf32c = utf8_get_char(in);
                *out = utf32c; ++out;
		++c;
		in = utf8_next_char(in);
	}
	*out = 0; // null terminate the string

	if (num_chars)
		*num_chars = c;
	return utf32;
}


/**
 * @brief Converts a string from UTF-32 to UTF-8.
 *
 * @param s         current IMLSession for allocating memory for the new
 *                  string. If %NULL, the new string will be allocated using
 *                  malloc().
 * @param utf32     a pointer to a UTF-32 string.
 * @param num_chars location to store the number of UTF-8 characters written,
 *                  or %NULL
 *
 * @returns a pointer to the newly allocated UTF-8 string. If an error occurs,
 * %NULL will be returned.
 */
UTF8CHAR *
utf32_to_utf8(iml_session_t *s, const UTF32CHAR *utf32, unsigned int *num_chars)
{
        const UTF32CHAR *in;
        int utf8_len = 0;
        UTF8CHAR *out, *utf8 = NULL;
        int c = 0;
        
        // Calculate how much bytes we need to allocate for the new string
        in = utf32;
        utf8_len = 0;
        while (*in) {
                if (*in < 0x80) {
                        utf8_len += 1;
                } else if (*in < 0x800) {
                        utf8_len += 2;
                } else if (*in < 0x10000) {
                        utf8_len += 3;
                } else if (*in < 0x200000) {
                        utf8_len += 4;
                } else if (*in < 0x4000000) {
                        utf8_len += 5;
                } else {
                        // outside range, technically max is only 0x110000
                        return NULL;
                }
                ++in;
        }

        if (s)
                utf8 = (UTF8CHAR *)s->If->m->iml_new(s, sizeof(UTF8CHAR) * utf8_len + 1);
        else
                utf8 = (UTF8CHAR *)malloc(sizeof(UTF8CHAR) * utf8_len + 1);
        memset(utf8, 0, sizeof(UTF8CHAR) * utf8_len + 1);

        // Now do the actual conversion
        in = utf32;
        out = utf8;
        while (*in) {
                UTF32CHAR u32 = *in;
                int mask, len, i;
                if (*in < 0x80) {
                        *out = (char)u32;
                        ++out;
                        ++in;
                        continue;
                } else if (*in < 0x800) {
                        len = 2;
                        mask = 0xC0;
                } else if (*in < 0x10000) {
                        len = 3;
                        mask = 0xE0;
                } else if (*in < 0x200000) {
                        len = 4;
                        mask = 0xF0;
                } else if (*in < 0x4000000) {
                        len = 5;
                        mask = 0xF8;
                }
                // construct the UTF-8 char backwards, easier
                for (i = len-1; i > 0; --i) {
                        out[i] = (u32 & 0x3F) | 0x80;
                        u32 >>= 6;
                }
                out[0] = u32 | mask;
                out += len;
                ++in;
        }
        *out = 0;

        if (num_chars)
                *num_chars = c;
        return utf8;
}


/**
 * @brief Converts a string from UTF-16BE to UTF-8.
 *
 * @param s         current IMLSession for allocating memory for the new
 *                  string. If %NULL, the new string will be allocated using
 *                  malloc().
 * @param utf16     a pointer to a UTF-16BE string.
 * @param num_chars location to store the number of UTF-8 characters written,
 *                  or %NULL
 *
 * @returns a pointer to the newly allocated UTF-8 string. If an error occurs,
 * %NULL will be returned.
 */
UTF8CHAR *
utf16be_to_utf8(iml_session_t *s, const UTFCHAR *utf16, unsigned int *num_chars)
{
        const UTFCHAR *in;
        int utf8_len = 0;
        UTF8CHAR *out, *utf8 = NULL;
        int c = 0;

        // Calculate how much space we need to allocate for the new string
        in = utf16;
        utf8_len = 0;
        while (*in) {
                UTF32CHAR utf32 = utf16_get_char(in);

                if (utf32 < 0x80) {
                        utf8_len += 1;
                } else if (utf32 < 0x800) {
                        utf8_len += 2;
                } else if (utf32 < 0x10000) {
                        utf8_len += 3;
                } else if (utf32 < 0x200000) {
                        utf8_len += 4;
                } else if (utf32 < 0x4000000) {
                        utf8_len += 5;
                } else {
                        // outside range, technically max is only 0x110000
                        return NULL;
                }
                in = utf16_next_char(in);
        }
       
        if (s)
                utf8 = (UTF8CHAR *)s->If->m->iml_new(s, sizeof(UTF8CHAR) * utf8_len + 1);
        else
                utf8 = (UTF8CHAR *)malloc(sizeof(UTF8CHAR) * utf8_len + 1);
        memset(utf8, 0, sizeof(UTF8CHAR) * utf8_len + 1);

        // Actual conversion
        in = utf16;
        out = utf8;
        while (*in) {
                UTF32CHAR utf32 = utf16_get_char(in);
                int mask, len, i;
                if (utf32 < 0x80) {
                        *out = (char)utf32;
                        ++out;
                        ++in;
                        continue;
                } else if (utf32 < 0x800) {
                        len = 2;
                        mask = 0xC0;
                } else if (utf32 < 0x10000) {
                        len = 3;
                        mask = 0xE0;
                } else if (utf32 < 0x200000) {
                        len = 4;
                        mask = 0xF0;
                } else if (utf32 < 0x4000000) {
                        len = 5;
                        mask = 0xF8;
                }
                // construct the UTF-8 char backwards, easier
                for (i = len-1; i > 0; --i) {
                        out[i] = (utf32 & 0x3F) | 0x80;
                        utf32 >>= 6;
                }
                out[0] = utf32 | mask;
                out += len;

                in = utf16_next_char(in);
        }
        *out = 0;

        if (num_chars)
                *num_chars = c;
        return utf8;
}

/** @} */
// vi:ts=4:sw=4:nowrap:cin:expandtab
